// Copyright 2020 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use std::collections::BTreeMap;

#[cfg(feature = "seccomp_trace")]
use base::debug;
use base::Event;
use devices::serial_device::SerialHardware;
use devices::serial_device::SerialParameters;
use devices::serial_device::SerialType;
use devices::Bus;
use devices::Serial;
use hypervisor::ProtectionType;
#[cfg(feature = "seccomp_trace")]
use jail::read_jail_addr;
#[cfg(windows)]
use jail::FakeMinijailStub as Minijail;
#[cfg(any(target_os = "android", target_os = "linux"))]
use minijail::Minijail;
use remain::sorted;
use thiserror::Error as ThisError;

use crate::DeviceRegistrationError;

mod sys;

/// Add the default serial parameters for serial ports that have not already been specified.
///
/// This ensures that `serial_parameters` will contain parameters for each of the four PC-style
/// serial ports (COM1-COM4).
///
/// It also sets the first `SerialHardware::Serial` to be the default console device if no other
/// serial parameters exist with console=true and the first serial device has not already been
/// configured explicitly.
pub fn set_default_serial_parameters(
    serial_parameters: &mut BTreeMap<(SerialHardware, u8), SerialParameters>,
    is_vhost_user_console_enabled: bool,
) {
    // If no console device exists and the first serial port has not been specified,
    // set the first serial port as a stdout+stdin console.
    let default_console = (SerialHardware::Serial, 1);
    if !serial_parameters.iter().any(|(_, p)| p.console) && !is_vhost_user_console_enabled {
        serial_parameters
            .entry(default_console)
            .or_insert(SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::Serial,
                name: None,
                path: None,
                input: None,
                num: 1,
                console: true,
                earlycon: false,
                stdin: true,
                out_timestamp: false,
                ..Default::default()
            });
    }

    // Ensure all four of the COM ports exist.
    // If one of these four SerialHardware::Serial port was not configured by the user,
    // set it up as a sink.
    for num in 1..=4 {
        let key = (SerialHardware::Serial, num);
        serial_parameters.entry(key).or_insert(SerialParameters {
            type_: SerialType::Sink,
            hardware: SerialHardware::Serial,
            name: None,
            path: None,
            input: None,
            num,
            console: false,
            earlycon: false,
            stdin: false,
            out_timestamp: false,
            ..Default::default()
        });
    }
}

/// Address for Serial ports in x86
pub const SERIAL_ADDR: [u64; 4] = [0x3f8, 0x2f8, 0x3e8, 0x2e8];

/// Information about a serial device (16550-style UART) created by `add_serial_devices()`.
pub struct SerialDeviceInfo {
    /// Address of the device on the bus.
    /// This is the I/O bus on x86 machines and MMIO otherwise.
    pub address: u64,

    /// Size of the device's address space on the bus.
    pub size: u64,

    /// IRQ number of the device.
    pub irq: u32,
}

/// Adds serial devices to the provided bus based on the serial parameters given.
///
/// Only devices with hardware type `SerialHardware::Serial` are added by this function.
///
/// # Arguments
///
/// * `protection_type` - VM protection mode.
/// * `io_bus` - Bus to add the devices to
/// * `com_evt_1_3` - irq and event for com1 and com3
/// * `com_evt_1_4` - irq and event for com2 and com4
/// * `serial_parameters` - definitions of serial parameter configurations.
/// * `serial_jail` - minijail object cloned for use with each serial device. All four of the
///   traditional PC-style serial ports (COM1-COM4) must be specified.
pub fn add_serial_devices(
    protection_type: ProtectionType,
    io_bus: &Bus,
    com_evt_1_3: (u32, &Event),
    com_evt_2_4: (u32, &Event),
    serial_parameters: &BTreeMap<(SerialHardware, u8), SerialParameters>,
    #[cfg_attr(windows, allow(unused_variables))] serial_jail: Option<Minijail>,
    #[cfg(feature = "swap")] swap_controller: &mut Option<swap::SwapController>,
) -> std::result::Result<Vec<SerialDeviceInfo>, DeviceRegistrationError> {
    let mut devices = Vec::new();
    for com_num in 0..=3 {
        let com_evt = match com_num {
            0 => &com_evt_1_3,
            1 => &com_evt_2_4,
            2 => &com_evt_1_3,
            3 => &com_evt_2_4,
            _ => &com_evt_1_3,
        };

        let (irq, com_evt) = (com_evt.0, com_evt.1);

        let param = serial_parameters
            .get(&(SerialHardware::Serial, com_num + 1))
            .ok_or(DeviceRegistrationError::MissingRequiredSerialDevice(
                com_num + 1,
            ))?;

        let mut preserved_descriptors = Vec::new();
        let com = param
            .create_serial_device::<Serial>(protection_type, com_evt, &mut preserved_descriptors)
            .map_err(DeviceRegistrationError::CreateSerialDevice)?;

        #[cfg(any(target_os = "android", target_os = "linux"))]
        let serial_jail = if let Some(serial_jail) = serial_jail.as_ref() {
            let jail_clone = serial_jail
                .try_clone()
                .map_err(DeviceRegistrationError::CloneJail)?;
            #[cfg(feature = "seccomp_trace")]
            debug!(
                    "seccomp_trace {{\"event\": \"minijail_clone\", \"src_jail_addr\": \"0x{:x}\", \"dst_jail_addr\": \"0x{:x}\"}}",
                    read_jail_addr(serial_jail),
                    read_jail_addr(&jail_clone)
                );
            Some(jail_clone)
        } else {
            None
        };
        #[cfg(windows)]
        let serial_jail = None;

        let com = sys::add_serial_device(
            com,
            param,
            serial_jail,
            preserved_descriptors,
            #[cfg(feature = "swap")]
            swap_controller,
        )?;

        let address = SERIAL_ADDR[usize::from(com_num)];
        let size = 0x8; // 16550 UART uses 8 bytes of address space.
        io_bus.insert(com, address, size).unwrap();
        devices.push(SerialDeviceInfo { address, size, irq })
    }

    Ok(devices)
}

#[sorted]
#[derive(ThisError, Debug)]
pub enum GetSerialCmdlineError {
    #[error("Error appending to cmdline: {0}")]
    KernelCmdline(kernel_cmdline::Error),
    #[error("Hardware {0} not supported as earlycon")]
    UnsupportedEarlyconHardware(SerialHardware),
}

pub type GetSerialCmdlineResult<T> = std::result::Result<T, GetSerialCmdlineError>;

/// Add serial options to the provided `cmdline` based on `serial_parameters`.
/// `serial_io_type` should be "io" if the platform uses x86-style I/O ports for serial devices
/// or "mmio" if the serial ports are memory mapped.
// TODO(b/227407433): Support cases where vhost-user console is specified.
pub fn get_serial_cmdline(
    cmdline: &mut kernel_cmdline::Cmdline,
    serial_parameters: &BTreeMap<(SerialHardware, u8), SerialParameters>,
    serial_io_type: &str,
    serial_devices: &[SerialDeviceInfo],
) -> GetSerialCmdlineResult<()> {
    for serial_parameter in serial_parameters
        .iter()
        .filter(|(_, p)| p.console)
        .map(|(k, _)| k)
    {
        match serial_parameter {
            (SerialHardware::Serial, num) => {
                cmdline
                    .insert("console", &format!("ttyS{}", num - 1))
                    .map_err(GetSerialCmdlineError::KernelCmdline)?;
            }
            (SerialHardware::VirtioConsole, num) => {
                cmdline
                    .insert("console", &format!("hvc{}", num - 1))
                    .map_err(GetSerialCmdlineError::KernelCmdline)?;
            }
            (SerialHardware::Debugcon, _) => {}
        }
    }

    match serial_parameters
        .iter()
        .filter(|(_, p)| p.earlycon)
        .map(|(k, _)| k)
        .next()
    {
        Some((SerialHardware::Serial, num)) => {
            if let Some(serial_device) = serial_devices.get(*num as usize - 1) {
                cmdline
                    .insert(
                        "earlycon",
                        &format!("uart8250,{},0x{:x}", serial_io_type, serial_device.address),
                    )
                    .map_err(GetSerialCmdlineError::KernelCmdline)?;
            }
        }
        Some((hw, _num)) => {
            return Err(GetSerialCmdlineError::UnsupportedEarlyconHardware(*hw));
        }
        None => {}
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use devices::BusType;
    use kernel_cmdline::Cmdline;

    use super::*;

    #[test]
    fn get_serial_cmdline_default() {
        let mut cmdline = Cmdline::new();
        let mut serial_parameters = BTreeMap::new();
        let io_bus = Bus::new(BusType::Io);
        let evt1_3 = Event::new().unwrap();
        let evt2_4 = Event::new().unwrap();

        set_default_serial_parameters(&mut serial_parameters, false);
        let serial_devices = add_serial_devices(
            ProtectionType::Unprotected,
            &io_bus,
            (4, &evt1_3),
            (3, &evt2_4),
            &serial_parameters,
            None,
            #[cfg(feature = "swap")]
            &mut None,
        )
        .unwrap();
        get_serial_cmdline(&mut cmdline, &serial_parameters, "io", &serial_devices)
            .expect("get_serial_cmdline failed");

        let cmdline_str = cmdline.as_str();
        assert!(cmdline_str.contains("console=ttyS0"));
    }

    #[test]
    fn get_serial_cmdline_virtio_console() {
        let mut cmdline = Cmdline::new();
        let mut serial_parameters = BTreeMap::new();
        let io_bus = Bus::new(BusType::Io);
        let evt1_3 = Event::new().unwrap();
        let evt2_4 = Event::new().unwrap();

        // Add a virtio-console device with console=true.
        serial_parameters.insert(
            (SerialHardware::VirtioConsole, 1),
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::VirtioConsole,
                num: 1,
                console: true,
                stdin: true,
                ..Default::default()
            },
        );

        set_default_serial_parameters(&mut serial_parameters, false);
        let serial_devices = add_serial_devices(
            ProtectionType::Unprotected,
            &io_bus,
            (4, &evt1_3),
            (3, &evt2_4),
            &serial_parameters,
            None,
            #[cfg(feature = "swap")]
            &mut None,
        )
        .unwrap();
        get_serial_cmdline(&mut cmdline, &serial_parameters, "io", &serial_devices)
            .expect("get_serial_cmdline failed");

        let cmdline_str = cmdline.as_str();
        assert!(cmdline_str.contains("console=hvc0"));
    }

    #[test]
    fn get_serial_cmdline_virtio_console_serial_earlycon() {
        let mut cmdline = Cmdline::new();
        let mut serial_parameters = BTreeMap::new();
        let io_bus = Bus::new(BusType::Io);
        let evt1_3 = Event::new().unwrap();
        let evt2_4 = Event::new().unwrap();

        // Add a virtio-console device with console=true.
        serial_parameters.insert(
            (SerialHardware::VirtioConsole, 1),
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::VirtioConsole,
                num: 1,
                console: true,
                stdin: true,
                ..Default::default()
            },
        );

        // Override the default COM1 with an earlycon device.
        serial_parameters.insert(
            (SerialHardware::Serial, 1),
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::Serial,
                num: 1,
                earlycon: true,
                ..Default::default()
            },
        );

        set_default_serial_parameters(&mut serial_parameters, false);
        let serial_devices = add_serial_devices(
            ProtectionType::Unprotected,
            &io_bus,
            (4, &evt1_3),
            (3, &evt2_4),
            &serial_parameters,
            None,
            #[cfg(feature = "swap")]
            &mut None,
        )
        .unwrap();
        get_serial_cmdline(&mut cmdline, &serial_parameters, "io", &serial_devices)
            .expect("get_serial_cmdline failed");

        let cmdline_str = cmdline.as_str();
        assert!(cmdline_str.contains("console=hvc0"));
        assert!(cmdline_str.contains("earlycon=uart8250,io,0x3f8"));
    }

    #[test]
    fn get_serial_cmdline_virtio_console_invalid_earlycon() {
        let mut cmdline = Cmdline::new();
        let mut serial_parameters = BTreeMap::new();
        let io_bus = Bus::new(BusType::Io);
        let evt1_3 = Event::new().unwrap();
        let evt2_4 = Event::new().unwrap();

        // Try to add a virtio-console device with earlycon=true (unsupported).
        serial_parameters.insert(
            (SerialHardware::VirtioConsole, 1),
            SerialParameters {
                type_: SerialType::Stdout,
                hardware: SerialHardware::VirtioConsole,
                num: 1,
                earlycon: true,
                stdin: true,
                ..Default::default()
            },
        );

        set_default_serial_parameters(&mut serial_parameters, false);
        let serial_devices = add_serial_devices(
            ProtectionType::Unprotected,
            &io_bus,
            (4, &evt1_3),
            (3, &evt2_4),
            &serial_parameters,
            None,
            #[cfg(feature = "swap")]
            &mut None,
        )
        .unwrap();
        get_serial_cmdline(&mut cmdline, &serial_parameters, "io", &serial_devices)
            .expect_err("get_serial_cmdline succeeded");
    }
}
